import type { Cluster, Redis } from "ioredis";

import type { DbService, DocItem, ExtraData, RedisSvcConfig } from "../types";
import { uuid } from "../utils";

export class RedisService implements DbService {

  private readonly redis: Cluster | Redis;

  private readonly key_prefix: string;

  private readonly lease: number;

  private readonly ttl: number;

  private readonly client_name: string;

  constructor(config: RedisSvcConfig) {
    this.redis = config.redis;
    this.key_prefix = config.key_prefix ? `${config.key_prefix.replace(/:*$/, "")}:` : "";
    this.lease = config.lease;
    this.ttl = config.ttl ?? 86400;
    this.client_name = config.client_name ?? "anonymous";
  }

  private readonly idToKey = (id: string): string => `${this.key_prefix}${id}`;

  public readonly getItem = async (id: string): Promise<DocItem | null> => {
    const doc = await this.redis.hgetall(this.idToKey(id)) as unknown as DocItem;
    return Object.keys(doc).length === 0 ? null : doc;
  };

  public readonly createNewLock = async (
    id: string,
    extra_data?: ExtraData
  ): Promise<string | null> => {
    /** revision version number */
    const rvn = uuid();
    const r = await this.redis.eval(`
if redis.call('hget', KEYS[1], 'rvn') == false then
    redis.call('hmset', KEYS[1], 'lease', ARGV[1], 'rvn', ARGV[2], `
    + `'client_name', ARGV[3]${extra_data ? ", 'extra_data', ARGV[5]" : ""})
    redis.call('expire', KEYS[1], ARGV[4])
    return 1
end return 0`, 1, this.idToKey(id), this.lease, rvn, this.client_name, this.ttl, JSON.stringify(extra_data)) as number;
    return r === 1 ? rvn : null;
  };

  public readonly grabLock = async (
    id: string,
    rvn: string,
    extra_data?: ExtraData
  ): Promise<string | null> => {
    const new_rvn = uuid();
    const r = await this.redis.eval(`
if redis.call('hget', KEYS[1], 'rvn') == ARGV[1] then
    redis.call('hset', KEYS[1], 'lease', ARGV[2], 'rvn', ARGV[3], 'client_name', ARGV[4]`
    + `${extra_data ? ", 'extra_data', ARGV[6]" : ""})
    redis.call('expire', KEYS[1], ARGV[5])
    return 1
end return 0`, 1, this.idToKey(id), rvn, this.lease, new_rvn, this.client_name, this.ttl, JSON.stringify(extra_data)
    ) as number;
    return r === 1 ? new_rvn : null;
  };

  public readonly sendHeartBeat = async (
    id: string,
    rvn: string
  ): Promise<string | null> => {
    const new_rvn = uuid();
    const r = await this.redis.eval(`
if redis.call('hget', KEYS[1], 'rvn') == ARGV[1] then
    redis.call('hset', KEYS[1], 'rvn', ARGV[2])
    redis.call('expire', KEYS[1], ARGV[3])
    return 1
end return 0`, 1, this.idToKey(id), rvn, new_rvn, this.ttl) as number;
    return r === 1 ? new_rvn : null;
  };

  public readonly releaseLock = async (
    id: string,
    rvn: string
  ): Promise<void> => {
    await this.redis.eval(`
if redis.call('hget', KEYS[1], 'rvn') == ARGV[1] then
    redis.call('del', KEYS[1])
    return 1
end return 0`, 1, this.idToKey(id), rvn);
  };
}
